﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using YAPSAR.Database;
using YAPSAR.Domain;
using YAPSAR.Analyzation;
using System.Drawing;

namespace YAPSAR.Collector
{
    public class SystemCollector
    {
        private static SystemCollector static_listener;
        public static SystemCollector Instance
        {
            get
            {
                if (static_listener == null)
                {
                    static_listener = new SystemCollector();
                }

                return static_listener;
            }
        }

        private KeyListener keyListener;
        private MouseListener mouseListener;

        private WorkUnit currentWorkUnit;

        private DateTime latestInteractionTime;
        private Timer interactionCheckTimer;
        private bool IsInactive
        {
            get
            {
                if (this.latestInteractionTime != null)
                {
                    var secondsDiff = this.GetSecondsBetweenDateTime(DateTime.Now, this.latestInteractionTime);
                    if (secondsDiff > Options.Instance.InactiveSecondsThreshold)
                    {
                        return true;
                    }
                }

                return false;
            }
        }

        private ProcessListener.ProcessInfoEventArgs latestProcessInfo;
        private Point latestMouseLocation;
        private StringBuilder typingBuffer = new StringBuilder();

        private SystemCollector()
        {
        }

        public void Start()
        {
            this.keyListener = new KeyListener();
            this.mouseListener = new MouseListener();

            this.keyListener.KeyPress += new KeyPressEventHandler(keyListener_KeyPress);
            this.mouseListener.OnMouseActivity += new MouseEventHandler(mouseListener_OnMouseActivity);
            ProcessListener.Instance.ActiveProcessChanged += new EventHandler<ProcessListener.ProcessInfoEventArgs>(processListener_ActiveProcessChanged);

            this.keyListener.Start();
            ProcessListener.Instance.Start();

            // Check for the latest interaction every 5 seconds.
            // And depending on what we need, we might show a GUI where we can explain what we've been away doing.
            this.interactionCheckTimer = new Timer();
            this.interactionCheckTimer.Interval = 5000;
            this.interactionCheckTimer.Tick += new EventHandler(interactionCheckTimer_Tick);
            this.interactionCheckTimer.Start();
        }

        public void Stop()
        {
            this.keyListener.Stop(false);
            this.mouseListener.Stop(false);
            ProcessListener.Instance.Stop();
            this.interactionCheckTimer.Stop();
            this.interactionCheckTimer.Dispose();

            // If there is something to save remaining in the work unit, then we will save it here.
            this.SaveAndClearCurrentWorkUnit();
        }

        private int GetSecondsBetweenDateTime(DateTime now, DateTime then)
        {
            var currentSpan = new TimeSpan(now.Ticks);
            var latestInteractionSpan = new TimeSpan(then.Ticks);

            var secondsDiff = currentSpan.TotalSeconds - latestInteractionSpan.TotalSeconds;
            return (int)secondsDiff;
        }

        private void ProcessAndClearTypingBuffer()
        {
            if (this.currentWorkUnit != null)
            {
                String word = typingBuffer.ToString().Trim();

                if (word.Length >= Options.Instance.TypedWordLengthThreshold)
                {
                    // We will only deal with the word if it is longer than X number of characters.
                    TypedWord typedWord = null;
                    foreach (var tw in currentWorkUnit.ProcessInstance.TypedWords)
                    {
                        if (tw.Word.Equals(word, StringComparison.InvariantCultureIgnoreCase))
                        {
                            typedWord = tw;
                            break;
                        }
                    }

                    if (typedWord == null)
                    {
                        typedWord = new TypedWord()
                        {
                            Frequency = 0,
                            Word = word
                        };

                        currentWorkUnit.ProcessInstance.TypedWords.Add(typedWord);
                    }

                    // Increment the word usage by 1 for each time we encounter it.
                    typedWord.Frequency = (typedWord.Frequency + 1);
                }
            }

            // Clear the typing buffer, even if there was no current work unit known.
            typingBuffer.Clear();
        }

        private void AddTypingBufferCharacter(char c)
        {
            // TODO: If the pressed key is "Ctrl+V" then we should read clipboard and append that content, and not clear buffer.
            if (this.currentWorkUnit != null)
            {
                this.currentWorkUnit.FrequencyKeyboard++;
            }

            if (c == '\b')
            {
                // The back key was pressed, and we support that by removing previous character from typing history.
                this.RemoveLastTypingBufferCharacter();
            }
            else if (Char.IsWhiteSpace(c) || Char.IsSymbol(c) || Char.IsSeparator(c) || Char.IsPunctuation(c) || Char.IsControl(c))
            {
                this.ProcessAndClearTypingBuffer();
            }
            else
            {
                this.typingBuffer.Append(c);
            }
        }

        private void RemoveLastTypingBufferCharacter()
        {
            if (this.typingBuffer.Length > 0)
            {
                this.typingBuffer.Remove(this.typingBuffer.Length - 1, 1);
            }
        }

        private void SaveAndClearCurrentWorkUnit()
        {
            if (this.currentWorkUnit != null)
            {
                this.currentWorkUnit.TimeTo = DateTime.Now;

                if (Options.Instance.PersistAllWords == false)
                {
                    // The option says that we do not want to persist the words, only use them during the work unit.
                    // So remove all the typed words before persisting.
                    this.currentWorkUnit.ProcessInstance.TypedWords.Clear();
                }
                else
                {
                    // We're supposed to persist the words, but there's no point in saving words that were written less than X times.
                    for (int i = 0; i < this.currentWorkUnit.ProcessInstance.TypedWords.Count; i++)
                    {
                        if (this.currentWorkUnit.ProcessInstance.TypedWords[i].Frequency < Options.Instance.TypedWordFrequencyThreshold)
                        {
                            // Remove the typed word, since it was in total written less than X times.
                            this.currentWorkUnit.ProcessInstance.TypedWords.RemoveAt(i);
                            i--;
                        }
                    }
                }

                using (var t = DatabaseManager.Instance.Session.BeginTransaction())
                {
                    if (this.currentWorkUnit.Id == 0)
                    {
                        int secondsDiff = GetSecondsBetweenDateTime(DateTime.Now, this.currentWorkUnit.TimeFrom);

                        if (secondsDiff > Options.Instance.PersistWorkUnitSecondsThreshold)
                        {
                            Console.WriteLine("Saving current work unit '" + this.currentWorkUnit + "'");
                            DatabaseManager.Instance.Session.Save(this.currentWorkUnit);
                            t.Commit();
                        }
                        else
                        {
                            Console.WriteLine("Not saving current work unit, since it was only '" + secondsDiff + "' seconds long");
                        }
                    }
                    else
                    {
                        Console.WriteLine("Merging current work unit '" + this.currentWorkUnit + "'");
                        DatabaseManager.Instance.Session.Merge(this.currentWorkUnit);
                        t.Commit();
                    }
                }

                this.currentWorkUnit = null;
            }
        }

        private void NotifyActionDone()
        {
            // Set the current time as the latest action time, so we can keep track of when the user was not at the computer.
            this.latestInteractionTime = DateTime.Now;

            if (this.interactionCheckTimer.Enabled == false)
            {
                // The interaction timer is currently not running, so we will start it again.
                // TODO: There needs to be a check so that we don't get several "You're back!" popups.
                this.interactionCheckTimer.Start();
            }
        }

        private void processListener_ActiveProcessChanged(object sender, ProcessListener.ProcessInfoEventArgs e)
        {
            this.NotifyActionDone();
            this.SaveAndClearCurrentWorkUnit();

            // Set the latest process info, which we might need later when we resume from inactivity after the work unit has been closed.
            this.latestProcessInfo = e;
            this.ProcessStarted();
        }

        private void ProcessStarted()
        {
            WorkUnit wu = new WorkUnit();
            wu.TimeFrom = DateTime.Now;
            wu.ProcessInstance = WorkUnitAnalyzer.Instance.GetProcessInstance(wu, this.latestProcessInfo.Title, this.latestProcessInfo.FilePath, this.latestProcessInfo.ProcessID);

            this.currentWorkUnit = wu;
        }

        private void mouseListener_OnMouseActivity(object sender, MouseEventArgs e)
        {
            this.NotifyActionDone();

            if (e.Button != MouseButtons.None)
            {
                // A mouse button has been clicked.
                // We cannot know if the user is still typing at the same location.
                // So we will have to remove the current typing history.
                this.ProcessAndClearTypingBuffer();
            }

            if (this.currentWorkUnit != null)
            {
                if (this.latestMouseLocation != null)
                {
                    int dragDistance = (int)Math.Abs(Math.Sqrt(Math.Pow((e.Location.X - latestMouseLocation.X), 2) + Math.Pow((e.Location.Y - latestMouseLocation.Y), 2)));
                    this.currentWorkUnit.DistanceMouseMoved += dragDistance;
                }

                switch (e.Button)
                {
                    case MouseButtons.Left:
                        this.currentWorkUnit.FrequencyMouseLeft++;
                        break;
                    case MouseButtons.Right:
                        this.currentWorkUnit.FrequencyMouseRight++;
                        break;
                    case MouseButtons.Middle:
                        this.currentWorkUnit.FrequencyMouseMiddle++;
                        break;
                }
            }

            this.latestMouseLocation = e.Location;
        }

        private void keyListener_KeyPress(object sender, KeyPressEventArgs e)
        {
            this.NotifyActionDone();

            this.AddTypingBufferCharacter(e.KeyChar);
        }

        private void interactionCheckTimer_Tick(object sender, EventArgs e)
        {
            if (this.IsInactive)
            {
                // Since we know that we're inactive, we can stop running the interaction check timer.
                // We will resume it later when we know we're active again.
                this.interactionCheckTimer.Stop();

                // We're currently inactive, so we will close the current work unit, and continue it later if resumed.
                this.SaveAndClearCurrentWorkUnit();

                // TODO: Show Form which notifies that you're currently inactive and should do X to resume; where the user can state what (s)he's been doing.
            }

            this.latestInteractionTime = DateTime.Now;
        }
    }
}
